[id].vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. <template>
  2. <div v-if="workflow" class="flex h-screen">
  3. <div
  4. v-if="state.showSidebar"
  5. class="w-80 bg-white dark:bg-gray-800 py-6 relative border-l border-gray-100 dark:border-gray-700 dark:border-opacity-50 flex flex-col"
  6. >
  7. <workflow-edit-block
  8. v-if="editState.editing"
  9. v-model:autocomplete="autocompleteState.cache"
  10. :data="editState.blockData"
  11. :data-changed="autocompleteState.dataChanged"
  12. :workflow="workflow"
  13. :editor="editor"
  14. @update="updateBlockData"
  15. @close="(editState.editing = false), (editState.blockData = {})"
  16. />
  17. <workflow-details-card
  18. v-else
  19. :workflow="workflow"
  20. @update="updateWorkflow"
  21. />
  22. </div>
  23. <div class="flex-1 relative overflow-auto">
  24. <div
  25. class="absolute w-full flex items-center z-10 left-0 p-4 top-0 pointer-events-none"
  26. >
  27. <ui-tabs
  28. v-model="state.activeTab"
  29. class="border-none px-2 rounded-lg h-full space-x-1 bg-white dark:bg-gray-800 pointer-events-auto"
  30. >
  31. <button
  32. v-tooltip="
  33. `${t('workflow.toggleSidebar')} (${
  34. shortcut['editor:toggle-sidebar'].readable
  35. })`
  36. "
  37. style="margin-right: 6px"
  38. @click="toggleSidebar"
  39. >
  40. <v-remixicon
  41. :name="state.showSidebar ? 'riSideBarFill' : 'riSideBarLine'"
  42. />
  43. </button>
  44. <ui-tab value="editor">{{ t('common.editor') }}</ui-tab>
  45. <ui-tab value="logs" class="flex items-center">
  46. {{ t('common.log', 2) }}
  47. <span
  48. v-if="workflowStates.length > 0"
  49. class="ml-2 p-1 text-center inline-block text-xs rounded-full bg-accent text-white dark:text-black"
  50. style="min-width: 25px"
  51. >
  52. {{ workflowStates.length }}
  53. </span>
  54. </ui-tab>
  55. </ui-tabs>
  56. <div class="flex-grow pointer-events-none" />
  57. <editor-local-actions
  58. :editor="editor"
  59. :workflow="workflow"
  60. :is-data-changed="state.dataChanged"
  61. @update="onActionUpdated"
  62. @modal="(modalState.name = $event), (modalState.show = true)"
  63. />
  64. </div>
  65. <ui-tab-panels
  66. v-model="state.activeTab"
  67. class="overflow-hidden h-full w-full"
  68. @drop="onDropInEditor"
  69. @dragend="clearHighlightedElements"
  70. @dragover.prevent="onDragoverEditor"
  71. >
  72. <ui-tab-panel cache value="editor" class="w-full">
  73. <workflow-editor
  74. v-if="state.workflowConverted"
  75. :id="route.params.id"
  76. :data="workflow.drawflow"
  77. :class="{ 'animate-blocks': state.animateBlocks }"
  78. class="h-screen"
  79. @init="onEditorInit"
  80. @edit="initEditBlock"
  81. @update:node="state.dataChanged = true"
  82. @delete:node="state.dataChanged = true"
  83. >
  84. <template #controls-append>
  85. <button
  86. v-tooltip="t('workflow.autoAlign.title')"
  87. class="control-button hoverable ml-2"
  88. @click="autoAlign"
  89. >
  90. <v-remixicon name="riMagicLine" />
  91. </button>
  92. </template>
  93. </workflow-editor>
  94. <editor-local-ctx-menu
  95. v-if="editor"
  96. :editor="editor"
  97. @copy="copySelectedElements"
  98. @paste="pasteCopiedElements"
  99. @duplicate="duplicateElements"
  100. />
  101. </ui-tab-panel>
  102. <ui-tab-panel value="logs" class="mt-24 container">
  103. <editor-logs
  104. :workflow-id="route.params.id"
  105. :workflow-states="workflowStates"
  106. />
  107. </ui-tab-panel>
  108. </ui-tab-panels>
  109. </div>
  110. </div>
  111. <ui-modal
  112. v-model="modalState.show"
  113. :content-class="activeWorkflowModal?.width || 'max-w-xl'"
  114. v-bind="activeWorkflowModal.attrs || {}"
  115. >
  116. <template v-if="activeWorkflowModal.title" #header>
  117. {{ activeWorkflowModal.title }}
  118. <a
  119. v-if="activeWorkflowModal.docs"
  120. :title="t('common.docs')"
  121. :href="activeWorkflowModal.docs"
  122. target="_blank"
  123. class="inline-block align-middle"
  124. >
  125. <v-remixicon name="riInformationLine" size="20" />
  126. </a>
  127. </template>
  128. <component
  129. :is="activeWorkflowModal.component"
  130. v-bind="{ workflow }"
  131. v-on="activeWorkflowModal?.events || {}"
  132. @update="updateWorkflow"
  133. @close="modalState.show = false"
  134. />
  135. </ui-modal>
  136. </template>
  137. <script setup>
  138. import {
  139. watch,
  140. provide,
  141. reactive,
  142. computed,
  143. onMounted,
  144. shallowRef,
  145. onBeforeUnmount,
  146. } from 'vue';
  147. import { useI18n } from 'vue-i18n';
  148. import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
  149. import { customAlphabet } from 'nanoid';
  150. import defu from 'defu';
  151. import dagre from 'dagre';
  152. import { useStore } from '@/stores/main';
  153. import { useUserStore } from '@/stores/user';
  154. import { useWorkflowStore } from '@/stores/workflow';
  155. import { useShortcut, getShortcut } from '@/composable/shortcut';
  156. import { tasks } from '@/utils/shared';
  157. import { debounce, parseJSON, throttle } from '@/utils/helper';
  158. import { fetchApi } from '@/utils/api';
  159. import browser from 'webextension-polyfill';
  160. import DroppedNode from '@/utils/editor/DroppedNode';
  161. import convertWorkflowData from '@/utils/convertWorkflowData';
  162. import WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';
  163. import WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';
  164. import WorkflowSettings from '@/components/newtab/workflow/WorkflowSettings.vue';
  165. import WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vue';
  166. import WorkflowDataTable from '@/components/newtab/workflow/WorkflowDataTable.vue';
  167. import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';
  168. import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
  169. import EditorLogs from '@/components/newtab/workflow/editor/EditorLogs.vue';
  170. import EditorLocalCtxMenu from '@/components/newtab/workflow/editor/EditorLocalCtxMenu.vue';
  171. import EditorLocalActions from '@/components/newtab/workflow/editor/EditorLocalActions.vue';
  172. const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 7);
  173. const { t } = useI18n();
  174. const store = useStore();
  175. const route = useRoute();
  176. const router = useRouter();
  177. const userStore = useUserStore();
  178. const workflowStore = useWorkflowStore();
  179. const editor = shallowRef(null);
  180. const state = reactive({
  181. showSidebar: true,
  182. dataChanged: false,
  183. animateBlocks: false,
  184. workflowConverted: false,
  185. activeTab: route.query.tab || 'editor',
  186. });
  187. const modalState = reactive({
  188. name: '',
  189. show: false,
  190. });
  191. const editState = reactive({
  192. blockData: {},
  193. editing: false,
  194. });
  195. const autocompleteState = reactive({
  196. cache: new Map(),
  197. dataChanged: false,
  198. });
  199. const workflowPayload = {
  200. data: {},
  201. isUpdating: false,
  202. };
  203. const workflowModals = {
  204. table: {
  205. icon: 'riKey2Line',
  206. width: 'max-w-2xl',
  207. component: WorkflowDataTable,
  208. title: t('workflow.table.title'),
  209. docs: 'https://docs.automa.site/api-reference/table.html',
  210. },
  211. 'workflow-share': {
  212. icon: 'riShareLine',
  213. component: WorkflowShare,
  214. attrs: {
  215. blur: true,
  216. persist: true,
  217. customContent: true,
  218. },
  219. events: {
  220. close() {
  221. modalState.show = false;
  222. modalState.name = '';
  223. },
  224. publish() {
  225. modalState.show = false;
  226. modalState.name = '';
  227. },
  228. },
  229. },
  230. 'global-data': {
  231. width: 'max-w-2xl',
  232. icon: 'riDatabase2Line',
  233. component: WorkflowGlobalData,
  234. title: t('common.globalData'),
  235. docs: 'https://docs.automa.site/api-reference/global-data.html',
  236. },
  237. settings: {
  238. width: 'max-w-2xl',
  239. icon: 'riSettings3Line',
  240. component: WorkflowSettings,
  241. title: t('common.settings'),
  242. attrs: {
  243. customContent: true,
  244. },
  245. events: {
  246. close() {
  247. modalState.show = false;
  248. modalState.name = '';
  249. },
  250. },
  251. },
  252. };
  253. const workflow = computed(() => workflowStore.getById(route.params.id));
  254. const workflowStates = computed(() =>
  255. workflowStore.getWorkflowStates(route.params.id)
  256. );
  257. const activeWorkflowModal = computed(
  258. () => workflowModals[modalState.name] || {}
  259. );
  260. provide('workflow', {
  261. editState,
  262. data: workflow,
  263. });
  264. provide('workflow-editor', editor);
  265. const updateBlockData = debounce((data) => {
  266. const node = editor.value.getNode.value(editState.blockData.blockId);
  267. const dataCopy = JSON.parse(JSON.stringify(data));
  268. if (editState.blockData.itemId) {
  269. const itemIndex = node.data.blocks.findIndex(
  270. ({ itemId }) => itemId === editState.blockData.itemId
  271. );
  272. if (itemIndex === -1) return;
  273. node.data.blocks[itemIndex].data = dataCopy;
  274. } else {
  275. node.data = dataCopy;
  276. }
  277. editState.blockData.data = data;
  278. state.dataChanged = true;
  279. }, 250);
  280. const updateHostedWorkflow = throttle(async () => {
  281. if (!userStore.user || workflowPayload.isUpdating) return;
  282. const isHosted = userStore.hostedWorkflows[route.params.id];
  283. const isBackup = userStore.backupIds.includes(route.params.id);
  284. const workflowExist = workflowStore.getById(route.params.id);
  285. if (
  286. (!isBackup && !isHosted) ||
  287. (workflowExist && Object.keys(workflowPayload.data).length === 0)
  288. )
  289. return;
  290. workflowPayload.isUpdating = true;
  291. const delKeys = [
  292. 'id',
  293. 'pass',
  294. 'logs',
  295. 'trigger',
  296. 'createdAt',
  297. 'isDisabled',
  298. 'isProtected',
  299. ];
  300. delKeys.forEach((key) => {
  301. delete workflowPayload.data[key];
  302. });
  303. try {
  304. if (typeof workflowPayload.data.drawflow === 'string') {
  305. workflowPayload.data.drawflow = parseJSON(
  306. workflowPayload.data.drawflow,
  307. workflowPayload.data.drawflow
  308. );
  309. }
  310. const response = await fetchApi(`/me/workflows/${route.params.id}`, {
  311. method: 'PUT',
  312. keepalive: true,
  313. body: JSON.stringify({
  314. workflow: workflowPayload.data,
  315. }),
  316. });
  317. if (!response.ok) throw new Error(response.message);
  318. if (isBackup) {
  319. const result = await response.json();
  320. if (result.updatedAt) {
  321. await browser.storage.local.set({ lastBackup: result.updatedAt });
  322. }
  323. }
  324. workflowPayload.data = {};
  325. workflowPayload.isUpdating = false;
  326. } catch (error) {
  327. console.error(error);
  328. workflowPayload.isUpdating = false;
  329. }
  330. }, 5000);
  331. const onNodesChange = debounce((changes) => {
  332. changes.forEach(({ type, id }) => {
  333. if (type === 'remove') {
  334. if (editState.blockData.blockId === id) {
  335. editState.editing = false;
  336. editState.blockData = {};
  337. }
  338. state.dataChanged = true;
  339. }
  340. });
  341. }, 250);
  342. const onEdgesChange = debounce((changes) => {
  343. changes.forEach(({ type }) => {
  344. if (state.dataChanged) return;
  345. state.dataChanged = type !== 'select';
  346. });
  347. }, 250);
  348. function autoAlign() {
  349. state.animateBlocks = true;
  350. const graph = new dagre.graphlib.Graph();
  351. graph.setGraph({
  352. rankdir: 'LR',
  353. ranksep: 100,
  354. ranker: 'tight-tree',
  355. });
  356. graph._isMultigraph = true;
  357. graph.setDefaultEdgeLabel(() => ({}));
  358. editor.value.getNodes.value.forEach(({ id, label, dimensions }) => {
  359. graph.setNode(id, {
  360. label,
  361. width: dimensions.width,
  362. height: dimensions.height,
  363. });
  364. });
  365. editor.value.getEdges.value.forEach(({ source, target, id }) => {
  366. graph.setEdge(source, target, { id });
  367. });
  368. dagre.layout(graph);
  369. const nodeChanges = graph.nodes().map((nodeId) => {
  370. const { x, y } = graph.node(nodeId);
  371. return {
  372. id: nodeId,
  373. type: 'position',
  374. dragging: false,
  375. position: { x, y },
  376. };
  377. });
  378. editor.value.applyNodeChanges(nodeChanges);
  379. editor.value.fitView();
  380. setTimeout(() => {
  381. state.dataChanged = true;
  382. state.animateBlocks = false;
  383. }, 500);
  384. }
  385. function toggleSidebar() {
  386. state.showSidebar = !state.showSidebar;
  387. localStorage.setItem('workflow:sidebar', state.showSidebar);
  388. }
  389. function initEditBlock(data) {
  390. const { editComponent, data: blockDefData } = tasks[data.id];
  391. const blockData = defu(data.data, blockDefData);
  392. editState.blockData = { ...data, editComponent, data: blockData };
  393. if (data.id === 'wait-connections') {
  394. const connections = editor.value.getEdges.value.reduce(
  395. (acc, { target, sourceNode, source }) => {
  396. if (target !== data.blockId) return acc;
  397. let name = t(`workflow.blocks.${sourceNode.label}.name`);
  398. const { description } = sourceNode.data;
  399. if (description) name += ` (${description})`;
  400. acc.push({
  401. name,
  402. id: source,
  403. });
  404. return acc;
  405. },
  406. []
  407. );
  408. editState.blockData.connections = connections;
  409. }
  410. editState.editing = true;
  411. }
  412. async function updateWorkflow(data) {
  413. try {
  414. await workflowStore.update({
  415. data,
  416. id: route.params.id,
  417. });
  418. workflowPayload.data = { ...workflowPayload.data, ...data };
  419. await updateHostedWorkflow();
  420. } catch (error) {
  421. console.error(error);
  422. }
  423. }
  424. function onActionUpdated({ data, changedIndicator }) {
  425. state.dataChanged = changedIndicator;
  426. workflowPayload.data = { ...workflowPayload.data, ...data };
  427. updateHostedWorkflow();
  428. }
  429. function onEditorInit(instance) {
  430. editor.value = instance;
  431. instance.onEdgeDoubleClick(({ edge }) => {
  432. instance.removeEdges([edge]);
  433. });
  434. instance.onEdgesChange(onEdgesChange);
  435. instance.onNodesChange(onNodesChange);
  436. instance.removeSelectedNodes(
  437. instance.getSelectedNodes.value.map(({ id }) => id)
  438. );
  439. instance.removeSelectedEdges(
  440. instance.getSelectedEdges.value.map(({ id }) => id)
  441. );
  442. const { blockId } = route.query;
  443. if (blockId) {
  444. const block = instance.getNode.value(blockId);
  445. if (!block) return;
  446. instance.addSelectedNodes([block]);
  447. setTimeout(() => {
  448. const editorContainer = document.querySelector('.vue-flow');
  449. const { height, width } = editorContainer.getBoundingClientRect();
  450. const { x, y } = block.position;
  451. instance.setTransform({
  452. y: -(y - height / 2),
  453. x: -(x - width / 2) - 200,
  454. zoom: 1,
  455. });
  456. }, 200);
  457. }
  458. }
  459. function clearHighlightedElements() {
  460. const elements = document.querySelectorAll(
  461. '.dropable-area__node, .dropable-area__handle'
  462. );
  463. elements.forEach((element) => {
  464. element.classList.remove('dropable-area__node');
  465. element.classList.remove('dropable-area__handle');
  466. });
  467. }
  468. function toggleHighlightElement({ target, elClass, classes }) {
  469. const targetEl = target.closest(elClass);
  470. if (targetEl) {
  471. targetEl.classList.add(classes);
  472. } else {
  473. const elements = document.querySelectorAll(`.${classes}`);
  474. elements.forEach((element) => {
  475. element.classList.remove(classes);
  476. });
  477. }
  478. }
  479. function onDragoverEditor({ target }) {
  480. toggleHighlightElement({
  481. target,
  482. elClass: '.vue-flow__handle.source',
  483. classes: 'dropable-area__handle',
  484. });
  485. if (!target.closest('.vue-flow__handle')) {
  486. toggleHighlightElement({
  487. target,
  488. elClass: '.vue-flow__node:not(.vue-flow__node-BlockGroup)',
  489. classes: 'dropable-area__node',
  490. });
  491. }
  492. }
  493. function onDropInEditor({ dataTransfer, clientX, clientY, target }) {
  494. const block = parseJSON(dataTransfer.getData('block'), null);
  495. if (!block) return;
  496. clearHighlightedElements();
  497. const nodeEl = DroppedNode.isNode(target);
  498. if (nodeEl) {
  499. DroppedNode.replaceNode(editor.value, { block, target: nodeEl });
  500. return;
  501. }
  502. const isTriggerExists =
  503. block.id === 'trigger' &&
  504. editor.value.getNodes.value.some((node) => node.label === 'trigger');
  505. if (isTriggerExists) return;
  506. const position = editor.value.project({ x: clientX - 360, y: clientY - 18 });
  507. const newNode = {
  508. position,
  509. id: nanoid(),
  510. label: block.id,
  511. data: block.data,
  512. type: block.component,
  513. };
  514. editor.value.addNodes([newNode]);
  515. const edgeEl = DroppedNode.isEdge(target);
  516. const handleEl = DroppedNode.isHandle(target);
  517. if (handleEl) {
  518. DroppedNode.appendNode(editor.value, {
  519. target: handleEl,
  520. nodeId: newNode.id,
  521. });
  522. } else if (edgeEl) {
  523. DroppedNode.insertBetweenNode(editor.value, {
  524. target: edgeEl,
  525. nodeId: newNode.id,
  526. outputs: block.outputs,
  527. });
  528. }
  529. if (block.fromGroup) {
  530. setTimeout(() => {
  531. const blockEl = document.querySelector(`[data-id="${newNode.id}"]`);
  532. blockEl?.setAttribute('group-item-id', block.itemId);
  533. }, 200);
  534. }
  535. state.dataChanged = true;
  536. }
  537. function copyElements(nodes, edges, initialPos) {
  538. const newIds = new Map();
  539. let firstNodePos = null;
  540. const newNodes = nodes.map(({ id, label, position, data, type }, index) => {
  541. const newNodeId = nanoid();
  542. const nodePos = {
  543. z: position.z || 0,
  544. y: position.y + 50,
  545. x: position.x + 50,
  546. };
  547. newIds.set(id, newNodeId);
  548. if (initialPos) {
  549. if (index === 0) {
  550. firstNodePos = {
  551. x: nodePos.x,
  552. y: nodePos.y,
  553. };
  554. initialPos = editor.value.project({
  555. y: initialPos.clientY,
  556. x: initialPos.clientX - 360,
  557. });
  558. Object.assign(nodePos, initialPos);
  559. } else {
  560. const xDistance = nodePos.x - firstNodePos.x;
  561. const yDistance = nodePos.y - firstNodePos.y;
  562. nodePos.x = initialPos.x + xDistance;
  563. nodePos.y = initialPos.y + yDistance;
  564. }
  565. }
  566. return {
  567. type,
  568. data,
  569. label,
  570. id: newNodeId,
  571. selected: true,
  572. position: nodePos,
  573. };
  574. });
  575. const newEdges = edges.reduce(
  576. (acc, { target, targetHandle, source, sourceHandle }) => {
  577. const targetId = newIds.get(target);
  578. const sourceId = newIds.get(source);
  579. if (!targetId || !sourceId) return acc;
  580. acc.push({
  581. selected: true,
  582. target: targetId,
  583. source: sourceId,
  584. id: `edge-${nanoid()}`,
  585. targetHandle: targetHandle.replace(target, targetId),
  586. sourceHandle: sourceHandle.replace(source, sourceId),
  587. });
  588. return acc;
  589. },
  590. []
  591. );
  592. return {
  593. nodes: newNodes,
  594. edges: newEdges,
  595. };
  596. }
  597. function duplicateElements({ nodes, edges }) {
  598. const selectedNodes = editor.value.getSelectedNodes.value;
  599. const selectedEdges = editor.value.getSelectedEdges.value;
  600. const { edges: newEdges, nodes: newNodes } = copyElements(
  601. nodes || selectedNodes,
  602. edges || selectedEdges
  603. );
  604. editor.value.removeSelectedNodes(selectedNodes);
  605. editor.value.removeSelectedEdges(selectedEdges);
  606. editor.value.addNodes(newNodes);
  607. editor.value.addEdges(newEdges);
  608. }
  609. function copySelectedElements(data = {}) {
  610. store.copiedEls.nodes = data.nodes || editor.value.getSelectedNodes.value;
  611. store.copiedEls.edges = data.edges || editor.value.getSelectedEdges.value;
  612. }
  613. function pasteCopiedElements(position) {
  614. editor.value.removeSelectedNodes(editor.value.getSelectedNodes.value);
  615. editor.value.removeSelectedEdges(editor.value.getSelectedEdges.value);
  616. const { nodes, edges } = copyElements(
  617. store.copiedEls.nodes,
  618. store.copiedEls.edges,
  619. position
  620. );
  621. editor.value.addNodes(nodes);
  622. editor.value.addEdges(edges);
  623. }
  624. function onKeydown({ ctrlKey, metaKey, key }) {
  625. const command = (keyName) => (ctrlKey || metaKey) && keyName === key;
  626. if (command('c')) {
  627. copySelectedElements();
  628. } else if (command('v')) {
  629. pasteCopiedElements();
  630. }
  631. }
  632. const shortcut = useShortcut([
  633. getShortcut('editor:toggle-sidebar', toggleSidebar),
  634. getShortcut('editor:duplicate-block', duplicateElements),
  635. ]);
  636. watch(
  637. () => state.activeTab,
  638. (value) => {
  639. router.replace({ ...route, query: { tab: value } });
  640. }
  641. );
  642. /* eslint-disable consistent-return */
  643. onBeforeRouteLeave(() => {
  644. updateHostedWorkflow();
  645. if (!state.dataChanged) return;
  646. const confirm = window.confirm(t('message.notSaved'));
  647. if (!confirm) return false;
  648. });
  649. onMounted(() => {
  650. if (!workflow.value) {
  651. router.replace('/');
  652. return null;
  653. }
  654. state.showSidebar =
  655. JSON.parse(localStorage.getItem('workflow:sidebar')) ?? true;
  656. const convertedData = convertWorkflowData(workflow.value);
  657. updateWorkflow({ drawflow: convertedData.drawflow }).then(() => {
  658. state.workflowConverted = true;
  659. });
  660. window.onbeforeunload = () => {
  661. updateHostedWorkflow();
  662. if (state.dataChanged) {
  663. return t('message.notSaved');
  664. }
  665. };
  666. window.addEventListener('keydown', onKeydown);
  667. });
  668. onBeforeUnmount(() => {
  669. window.onbeforeunload = null;
  670. window.removeEventListener('keydown', onKeydown);
  671. });
  672. </script>
  673. <style>
  674. .vue-flow,
  675. .editor-tab {
  676. width: 100%;
  677. height: 100%;
  678. }
  679. .vue-flow__node {
  680. @apply rounded-lg;
  681. }
  682. .dropable-area__node,
  683. .dropable-area__handle {
  684. @apply ring-4;
  685. }
  686. .animate-blocks {
  687. .vue-flow__transformationpane,
  688. .vue-flow__node {
  689. transition: transform 300ms ease;
  690. }
  691. }
  692. </style>